/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package eu.anorien.dmo.gui.optimization;

import eu.anorien.dmo.Function;
import eu.anorien.dmo.Parameter;
import eu.anorien.dmo.Server;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.apache.log4j.Logger;

/**
 *
 * @author David Miguel Antunes <davidmiguel [ at ] antunes.net>
 */
public class SimulatedAnnealingOptimization extends Optimization {

    private class SimulatedAnnealingOptimizer {

        private ArrayList<Integer> currentPoint;
        private double currentPointError;
        private Map<ArrayList<Integer>, Double> neighbors;
        private final Object searchThreadWait = new Object();
        private final Object coordinatorThreadWait = new Object();
        private int nSearchThreads;
        private double temperature;
        private double temperatureDecrementFactor;

        class SearchThread extends Thread {

            @Override
            public void run() {
                ArrayList<Integer> nextNeighbor = null;
                while (!terminate) {
                    if (nextNeighbor == null) {
                        // Try to get a next neighbor:
                        nextNeighbor = nextNeighbor(nextNeighbor);
                    }
                    if (nextNeighbor != null) {
                        if (random.nextDouble() > temperature) {
                            processNeighbor(nextNeighbor);
                            nextNeighbor = null;
                        } else {
                            randomExploration();
                        }
                    } else {
                        randomExploration();
                    }
                    temperature *= temperatureDecrementFactor;
                    output.write("Temperature is now " + temperature);
                }
            }

            private ArrayList<Integer> nextNeighbor(ArrayList<Integer> neighborPoint) {
                synchronized (SimulatedAnnealingOptimizer.this) {
                    for (Map.Entry<ArrayList<Integer>, Double> entry : neighbors.entrySet()) {
                        if (entry.getValue() == null) {
                            neighborPoint = entry.getKey();
                            neighbors.put(neighborPoint, new Double(-1));
                            break;
                        }
                    }
                }
                return neighborPoint;
            }

            private void processNeighbor(ArrayList<Integer> neighborPoint) {
                double error = -2;
                synchronized (errorValueCache) {
                    if (errorValueCache.get(neighborPoint) != null) {
                        error = errorValueCache.get(neighborPoint);
                        output.write("Cache hit.");
                    }
                }
                if (error == -2) {
                    error = runFunction(pointToParams(neighborPoint));
                    synchronized (errorValueCache) {
                        errorValueCache.put(neighborPoint, error);
                    }
                }
                output.write("Neighbor [" + paramsToString(pointToParams(neighborPoint)) + "] has a value of " + (1 / error));
                synchronized (SimulatedAnnealingOptimizer.this) {
                    if (neighbors.containsKey(neighborPoint)) {
                        neighbors.put(neighborPoint, error);
                    } else {
                        logger.debug("Neighbors seem to have changed.");
                    }
                }
                synchronized (coordinatorThreadWait) {
                    coordinatorThreadWait.notifyAll();
                }
            }

            private void randomExploration() {
                ArrayList<Integer> tentative = newRandomPoint();
                output.write("Exploring paramaters [" + paramsToString(pointToParams(tentative)) + "]");
                double error = -2;
                synchronized (errorValueCache) {
                    if (errorValueCache.get(tentative) != null) {
                        error = errorValueCache.get(tentative);
                        output.write("Cache hit.");
                    }
                }
                if (error == -2) {
                    error = runFunction(pointToParams(tentative));
                    synchronized (errorValueCache) {
                        errorValueCache.put(tentative, error);
                    }
                }
                output.write("Explored point [" + paramsToString(pointToParams(tentative)) + "] has a value of " + (1 / error));
                synchronized (SimulatedAnnealingOptimizer.this) {
                    if (error < currentPointError) {
                        for (Double e : neighbors.values()) {
                            if (e != null && error > e) {
                                break;
                            }
                        }
                        // Found a better point, changing to it:
                        currentPoint = tentative;
                        currentPointError = error;
                        output.write("Found a better point while exploring: [" + paramsToString(pointToParams(currentPoint)) + "] with a value of " + (1 / currentPointError));
                        neighbors = new HashMap<ArrayList<Integer>, Double>();
                        for (ArrayList<Integer> neighbor : neighborPoints(tentative)) {
                            neighbors.put(neighbor, null);
                        }
                    }
                }
            }
        }

        class CoordinatorThread extends Thread {

            @Override
            public void run() {
                outter:
                while (!terminate) {
                    synchronized (coordinatorThreadWait) {
                        try {
                            if (Thread.interrupted()) {
                                throw new InterruptedException();
                            }
                            coordinatorThreadWait.wait();
                        } catch (InterruptedException ex) {
                        }
                    }

                    synchronized (SimulatedAnnealingOptimizer.this) {
                        Map.Entry<ArrayList<Integer>, Double> best = neighbors.entrySet().iterator().next();
                        for (Map.Entry<ArrayList<Integer>, Double> entry : neighbors.entrySet()) {
                            if (entry.getValue() == null || entry.getValue() == -1) {
                                continue outter;
                            } else {
                                if (entry.getValue() < best.getValue()) {
                                    best = entry;
                                }
                            }
                        }
                        // All neighbors are processed:
                        if (currentPointError > best.getValue()) {
                            output.write("Reached local maximum of " + (1 / currentPointError) + " with params [" + paramsToString(pointToParams(currentPoint)) + "]" + "\n"
                                    + "Waitting for exploration to find a better value");
                        } else {
                            currentPoint = best.getKey();
                            currentPointError = best.getValue();
                            output.write("Next point is [" + paramsToString(pointToParams(currentPoint)) + "] with a value of " + (1 / currentPointError));
                            neighbors = new HashMap<ArrayList<Integer>, Double>();
                            for (ArrayList<Integer> neighbor : neighborPoints(best.getKey())) {
                                neighbors.put(neighbor, null);
                            }
                        }
                    }
                    synchronized (searchThreadWait) {
                        searchThreadWait.notifyAll();
                    }
                }
            }
        }

        public SimulatedAnnealingOptimizer(int nSearchThreads, double temperature, double temperatureDecrementFactor) {
            this.nSearchThreads = nSearchThreads;
            this.temperature = temperature;
            this.temperatureDecrementFactor = temperatureDecrementFactor;
            init();
        }

        public void start() {
            for (int i = 0; i < nSearchThreads; i++) {
                new SearchThread().start();
            }
            new CoordinatorThread().start();
        }

        private void init() {
            currentPoint = newRandomPoint();
            output.write("Starting new search at point [" + paramsToString(pointToParams(currentPoint)) + "]");
            currentPointError = -1;
            neighbors = new HashMap<ArrayList<Integer>, Double>();
            for (ArrayList<Integer> neighbor : neighborPoints(currentPoint)) {
                neighbors.put(neighbor, null);
            }
        }
    }
    private static final Logger logger = Logger.getLogger(HillClimbingOptimization.class);
    private final HashMap<ArrayList<Integer>, Double> errorValueCache = new HashMap<ArrayList<Integer>, Double>();
    private SimulatedAnnealingOptimizationConfigPanel configPanel;
    private int maxSpeed = 1;

    public SimulatedAnnealingOptimization(Function function, ArrayList<Server> servers) {
        super(function, servers);
        this.function = function;
        configPanel = new SimulatedAnnealingOptimizationConfigPanel(this);
        tabbedPane.add(configPanel, 0);
        tabbedPane.setTitleAt(0, "Configuration");
        tabbedPane.setSelectedIndex(0);
    }

    private List<ArrayList<Integer>> neighborPoints(List<Integer> point) {
        if (point.isEmpty()) {
            ArrayList<ArrayList<Integer>> points = new ArrayList<ArrayList<Integer>>();
            points.add(new ArrayList<Integer>());
            return points;
        } else {
            int i = point.get(0);
            ArrayList<ArrayList<Integer>> rslt = new ArrayList<ArrayList<Integer>>();
            List<ArrayList<Integer>> neighbors = neighborPoints(point.subList(1, point.size()));

            Parameter parameter = function.getParameters().get(function.getParameters().size() - point.size());

            if (i > 0) {
                for (ArrayList<Integer> arrayList : neighbors) {
                    ArrayList<Integer> newArrayList = new ArrayList<Integer>(arrayList);
                    newArrayList.add(0, Math.max(i - (random.nextInt(maxSpeed) + 1), 0));
                    rslt.add(newArrayList);
                }
            }
            for (ArrayList<Integer> arrayList : neighbors) {
                ArrayList<Integer> newArrayList = new ArrayList<Integer>(arrayList);
                newArrayList.add(0, i);
                rslt.add(newArrayList);
            }
            if (i < parameter.getNSteps() - 1) {
                for (ArrayList<Integer> arrayList : neighbors) {
                    ArrayList<Integer> newArrayList = new ArrayList<Integer>(arrayList);
                    newArrayList.add(0, Math.min(parameter.getNSteps(), i + (random.nextInt(maxSpeed) + 1)));
                    rslt.add(newArrayList);
                }
            }
            return rslt;
        }
    }

    @Override
    public void stop() {
        configPanel.getStopButton().setEnabled(false);
        super.stop();
    }

    @Override
    public void start() {
        super.start();
        configPanel.getStartButton().setEnabled(false);
        maxSpeed = configPanel.getMaxSpeed();
        final int nServersPerGradientDescent = configPanel.getNumServersPerProbe();
        output.write("Starting optimization..." + "\n");

        random = new Random(configPanel.getRandomSeed());
        stopAfter = configPanel.getStopAfter();

        SimulatedAnnealingOptimizer gradientDescentOptimizer = new SimulatedAnnealingOptimizer(nServersPerGradientDescent, configPanel.getInitialTemperature(), configPanel.getTemperatureUpdate());
        gradientDescentOptimizer.start();
        configPanel.getStopButton().setEnabled(true);
    }

    @Override
    public String toString() {
        return function.getName() + " Simulated Annealing";
    }
}
